/**
 * onedrive file sync
 */
import * as Msal from 'msal'
import { items as oneDriveAPI } from 'onedrive-api'
import axios from 'axios'
import LZString from 'lz-string'
import str from 'string-to-stream'
import cfg from '@/config'
import { localStore, sessionStore } from '@/utils/store'
import Database from './database'
import { PM_REMOTE_DATA, PM_MASTER_CODE_KEY } from '@/js/constants'

const STORAGE_KEY_LOADING = 'PASSWORD_MASTER_LOADING'

class OneDriveFileSync {
  constructor(msalConfig = {}) {
    this.msalConfig = msalConfig
    this.tokens = {
      login: {
        scopes: ['onedrive.appfolder'],
        accessToken: ''
      },
      appFolder: {
        scopes: ['Files.ReadWrite.AppFolder'],
        accessToken: ''
      }
    }
    this.loginInfo = null
    this.appFolderInfo = null
    this.msalInstance = null
    this.database = null
  }
  init() {
    return new Promise(async (resolve, reject) => {
      this.msalInstance = new Msal.UserAgentApplication(this.msalConfig)
      let isLoading = localStore.get(STORAGE_KEY_LOADING)
      if (!isLoading) {
        isLoading = true
        localStore.set(STORAGE_KEY_LOADING, isLoading)
        return
      }
      try {
        await this.login()
        this.database = new Database({
          appVersionCode: cfg.defaultFileContent.appVersionCode,
          account: this.msalInstance.getAccount().userName,
          // eslint-disable-next-line no-undef
          env: process.env.NODE_ENV
        })
        await this.getAppFolderInfo()
        resolve()
      } catch (err) {
        reject(err)
      }
    })
  }
  get(next = () => {}, { useCache = true } = {}) {
    const localRemoteData = localStore.get(PM_REMOTE_DATA)
    const remotePromise = new Promise(async (resolve, reject) => {
      this.getPasswordCloud()
        .then(async (res) => {
          // send notification
          try {
            localStore.set(PM_REMOTE_DATA, { ...res, records: [], masterCode: null })
            sessionStore.set(PM_MASTER_CODE_KEY, res.masterCode)
            await this.database.updateRecords(res.records)
          } catch (err) {
            reject(err)
          }
          resolve(res)
        })
    })
    if (useCache) {
      this.database.selectAll().then((records) => {
        const masterCode = sessionStore.get(PM_MASTER_CODE_KEY)
        if (localRemoteData) {
          next({ ...localRemoteData, records, masterCode, remotePromise })
        }
      })
    }
  }
  save(next = () => {}, data) {
    const localRemoteData = localStore.get(PM_REMOTE_DATA)
    try {
      if (data.versionCode > localRemoteData.versionCode) {
        // FIXME: try to get online data and merge to upload data
        localStore.set(PM_REMOTE_DATA, { ...data, records: [], masterCode: null })
        this.database.updateRecords(data.records)
      }
    } catch (err) {
      console.log(err)
    }
    // sync data
    if (
      localRemoteData &&
      data.versionCode === localRemoteData.versionCode &&
      data.records.length === 0
    ) {
      next()
      return
    }
    this.createPasswordFile(data)
      .then(res => next(res))
  }
  login() {
    return new Promise(async (resolve, reject) => {
      this.loginInfo = this.msalInstance.getAccount()
      if (!this.loginInfo) {
        // maybe expired, relogin
        // login
        const request = {
          scopes: this.tokens.login.scopes
        }
        try {
          const loginInfo = await this.msalInstance.loginPopup(request)
          this.loginInfo = loginInfo
        } catch (err) {
          reject(err)
        }
      } else if (OneDriveFileSync.checkIsLoginPopupPage()) {
        // has been logined
        // close popup page
        const isLoading = false
        localStore.set(STORAGE_KEY_LOADING, isLoading)
        OneDriveFileSync.closeCurrPage()
      }

      resolve(this.loginInfo)
    })
  }
  getAppFolderInfo(refresh = false) {
    return new Promise(async (resolve, reject) => {
      if (!this.appFolderInfo || refresh) {
        try {
          const accessToken = await this.getAppFolderToken()
          const { value } = await oneDriveAPI.listChildren({
            accessToken,
            itemId: 'root'
          })
          let appFolderInfo = value.find(_ => _.name === cfg.app.name)
          if (!appFolderInfo) {
            // 请求创建
            appFolderInfo = await this.createAppFolder()
          }
          this.appFolderInfo = appFolderInfo
          resolve(this.appFolderInfo)
        } catch (err) {
          reject(err)
        }
        return
      }
      resolve(this.appFolderInfo)
    })
  }
  getPasswordCloud() {
    return new Promise(async (resolve, reject) => {
      try {
        const accessToken = await this.getAppFolderToken()
        const { value } = await oneDriveAPI.listChildren({
          accessToken,
          itemId: this.appFolderInfo.id
        })
        const fileItem = value.find(_ => _.name === 'password.pm')
        if (fileItem) {
          const response = await axios({
            url: `https://graph.microsoft.com/v1.0/me/drive/items/${fileItem.id}/content?ts=${Date.now()}`,
            headers: {
              Authorization: `Bearer ${accessToken}`
            },
            method: 'GET',
            responseType: 'blob'
          })
          const blob = new Blob([response.data], { type: 'plain' })
          const reader = new FileReader()
          reader.addEventListener('loadend', (e) => {
            const passwordContent = OneDriveFileSync.deCompressStr(e.target.result)
            resolve(passwordContent)
          })
          reader.readAsText(blob)
        } else {
          // create password file
          await this.createPasswordFile()
          // return default password info
          resolve(cfg.defaultFileContent)
        }
      } catch (err) {
        reject(err)
      }
    })
  }
  createPasswordFile(content) {
    return new Promise(async (resolve, reject) => {
      // check file is exist
      try {
        const fileContent = content || cfg.defaultFileContent
        const compressStr = OneDriveFileSync.compressStr(fileContent)
        // check if decomporess is null
        if (!LZString.decompressFromUTF16(compressStr)) {
          reject(new Error(null))
          return
        }
        const stream = str(compressStr)
        const accessToken = await this.getAppFolderToken()
        const res = await oneDriveAPI.uploadSimple({
          accessToken,
          filename: 'password.pm',
          parentId: this.appFolderInfo.id,
          readableStream: stream
        })
        resolve(res)
      } catch (err) {
        reject(err)
      }
    })
  }
  static checkIsLoginPopupPage() {
    if (!process.env.isMiniprogram) {
      return window.location.hash.includes('id_token')
    }
    return false
  }
  static closeCurrPage() {
    if (!process.env.isMiniprogram) {
      window.opener = null
      window.open('', '_self', '')
      window.close()
    }
  }
  getAppFolderToken(refresh = false) {
    return new Promise(async (resolve, reject) => {
      if (!this.tokens.appFolder.accessToken || refresh) {
        const request = {
          scopes: this.tokens.appFolder.scopes
        }
        try {
          const { accessToken } = await this.msalInstance.acquireTokenSilent(request)
          this.tokens.appFolder.accessToken = accessToken
        } catch (err) {
          if (err.name === 'InteractionRequiredAuthError') {
            try {
              const { accessToken } = await this.msalInstance.acquireTokenPopup(request)
              this.tokens.appFolder.accessToken = accessToken
            } catch (err2) {
              reject(err2)
              return
            }
          }
          reject(err)
          return
        }
      }
      resolve(this.tokens.appFolder.accessToken)
    })
  }
  createAppFolder() {
    return new Promise(async (resolve, reject) => {
      try {
        const accessToken = await this.getAppFolderToken()
        const res = await oneDriveAPI.createFolder({
          accessToken,
          rootItemId: 'root',
          name: cfg.app.name
        })
        resolve(res)
      } catch (err) {
        reject(err)
      }
    })
  }
  static compressStr(objectContent = {}) {
    return LZString.compressToUTF16(JSON.stringify(objectContent))
  }
  static deCompressStr(jsonStr) {
    return JSON.parse(LZString.decompressFromUTF16(jsonStr))
  }
}

export default new OneDriveFileSync(cfg.msalConfig)
